#include "il2cpp-config.h"

#if IL2CPP_TARGET_POSIX

#include <cassert>
#include <cerrno>
#include <ctime>

#include "PosixWaitObject.h"
#include "PosixHelpers.h"
#include "ThreadImpl.h"
#include "os/Time.h"


// Notes:
// - Situation
//	 - None of the pthread APIs are interruptible (they all explicitly forbid returning EINTR).
//   - We cannot do any non-local transfers of control from signal handlers safely (C++ exceptions
//	   or longjmp). Thus we cannot use signals to inject interruptions into a thread.
//	 - Very few of the system APIs we have available support timeouts (at least not on all platforms).
// - Ergo: we need to roll our own synchronization primitives based on pthread condition variables
//     (they support timeouts and have the functionality needed to model the other primitives).
// - BUT: the condition variables still involve mutexes which we cannot lock in way that allows
//     interruptions. This means that there will be time windows where threads will wait and just
//     block and not allow interruption.


namespace il2cpp
{
namespace os
{
namespace posix
{

static pthread_mutex_t s_WaitObjectDeletionLock = PTHREAD_MUTEX_INITIALIZER;

PosixWaitObject::PosixWaitObject (Type type)
	: m_Type (type)
	, m_Count (0)
	, m_WaitingThreadCount (0)
{
	pthread_mutex_init (&m_Mutex, NULL);
#if IL2CPP_TARGET_JAVASCRIPT
	////FIXME: WebGL bombs in pthread_cond_init() with non-NULL attributes ATM.
	//// We need to fix this once we have proper pthreads on Emscripten. Right now
	//// this will set up the condition variable incorrectly.
	pthread_cond_init (&m_Condition, NULL);
#elif !IL2CPP_USE_POSIX_COND_TIMEDWAIT_REL
	// Initialize condition to use CLOCK_MONOTONIC instead of CLOCK_REALTIME.
	pthread_condattr_t attr;
	int result = pthread_condattr_init (&attr);
	pthread_condattr_setclock (&attr, CLOCK_MONOTONIC);
	pthread_cond_init (&m_Condition, &attr);
	pthread_condattr_destroy (&attr);
	pthread_cond_init (&m_Condition, NULL);
#else
	// On OSX and iOS we can use pthread_cond_timedwait_relative_np.
	pthread_cond_init (&m_Condition, NULL);
#endif
}

PosixWaitObject::~PosixWaitObject ()
{
	// Make sure it's okay to delete wait objects.
	AutoLockWaitObjectDeletion lockDeletion;

	pthread_mutex_destroy (&m_Mutex);
	pthread_cond_destroy (&m_Condition);
}

void PosixWaitObject::LockWaitObjectDeletion ()
{
	pthread_mutex_lock (&s_WaitObjectDeletionLock);
}

void PosixWaitObject::UnlockWaitObjectDeletion ()
{
	pthread_mutex_unlock (&s_WaitObjectDeletionLock);
}

WaitStatus PosixWaitObject::Wait (bool interruptible)
{
	return Wait (kNoTimeout, interruptible);
}

WaitStatus PosixWaitObject::Wait (uint32_t timeoutMS, bool interruptible)
{
	// IMPORTANT: This function must be exception-safe! APCs may throw.

	ThreadImpl* currentThread = ThreadImpl::GetCurrentThread ();

	// Do up-front check about pending APC except this is a zero-timeout
	// wait (i.e. a wait that is never supposed to block and thus go into
	// an interruptible state).
	if (interruptible && timeoutMS != 0)
		currentThread->CheckForUserAPCAndHandle ();

	// Lock object. We release this mutex during waiting.
	posix::PosixAutoLock lock (&m_Mutex);

	// See if the object is in a state where we can acquire it right away.
	if (m_Count == 0)
	{
		// No, hasn't. If we're not supposed to wait, we're done.
		if (timeoutMS == 0)
			return kWaitStatusTimeout;

		try
		{
			// We should wait. Let the world know this thread is now waiting
			// on this object.
			if (interruptible)
				currentThread->SetWaitObject (this);

			// Check APC queue again to avoid race condition.
			if (interruptible)
				currentThread->CheckForUserAPCAndHandle ();

			// Go into wait until we either have a release or timeout or otherwise fail.
			uint32_t remainingWaitTime = timeoutMS;
			WaitStatus waitStatus = kWaitStatusSuccess;
			while (m_Count == 0)
			{
				if (timeoutMS == posix::kNoTimeout)
				{
					// Infinite wait. Can only be interrupted by APC.
					++m_WaitingThreadCount; // No synchronization necessary; we hold the mutex.
					int status = pthread_cond_wait (&m_Condition, &m_Mutex);
					--m_WaitingThreadCount;

					if (status != 0)
					{
						waitStatus = kWaitStatusFailure;
						break;
					}
				}
				else
				{
					// Timed wait. Can be interrupted by APC or timeout.
					uint32_t waitStartTime = Time::GetTicksMillisecondsMonotonic ();
					timespec timeout = posix::MillisecondsToTimespec (remainingWaitTime);

#if !IL2CPP_USE_POSIX_COND_TIMEDWAIT_REL
					////NOTE: this codepath has not been tested!
					timespec waitStartTimeSpec = posix::MillisecondsToTimespec (waitStartTime);
					timeout.tv_sec += waitStartTimeSpec.tv_sec;
					timeout.tv_nsec += waitStartTimeSpec.tv_nsec;
#endif

					++m_WaitingThreadCount;
#if !IL2CPP_USE_POSIX_COND_TIMEDWAIT_REL
					// For plain POSIX, do an absolute timed wait.
					int status = pthread_cond_timedwait (&m_Condition, &m_Mutex, &timeout);
#else
					// For OSX and iOS, do a relative timed wait.
					int status = pthread_cond_timedwait_relative_np (&m_Condition, &m_Mutex, &timeout);
#endif
					--m_WaitingThreadCount; ////TODO: make this atomic for when we fail to reacquire the mutex

					if (status == ETIMEDOUT)
					{
						waitStatus = kWaitStatusTimeout;
						break;
					}
					else if (status != 0)
					{
						waitStatus = kWaitStatusFailure;
						break;
					}

					// Update time we have have left to wait.
					uint32_t waitTimeThisRound = Time::GetTicksMillisecondsMonotonic () - waitStartTime;
					if (waitTimeThisRound > remainingWaitTime)
						remainingWaitTime = 0;
					else
						remainingWaitTime -= waitTimeThisRound;
				}

				// We've received a signal but it may be because of an APC and not because
				// the semaphore got signaled. If so, handle the APC and go back to waiting.
				if (interruptible)
					currentThread->CheckForUserAPCAndHandle ();
			}

			// We're done waiting so untie us from the current thread.
			// NOTE: A thread may have grabbed us and then got paused. If we return now and then our owner
			//       tries to delete us, we would pull the rug from under the other thread. This is prevented by
			//       having a central lock on wait object deletion which any thread trying to deal with wait
			//       objects from other threads has to acquire.
			if (interruptible)
			{
				currentThread->SetWaitObject (NULL);

				// Avoid race condition by checking APC queue again after unsetting wait object.
				currentThread->CheckForUserAPCAndHandle ();
			}

			// If we failed, bail out now.
			if (waitStatus != kWaitStatusSuccess)
				return waitStatus;

		}
		catch (...)
		{
			if (interruptible)
				currentThread->SetWaitObject (NULL);
			throw;
		}
	}

	// At this point, we should be in signaled state and have the lock on
	// the object.

	// Object has been released. Acquire it for this thread.
	assert (m_Count > 0);
	switch (m_Type)
	{
		case kManualResetEvent:
			// Nothing to do.
			break;

		case kMutex:
		case kAutoResetEvent:
			m_Count = 0;
			break;

		case kSemaphore:
			if (m_Count > 0) // Defensive.
			{
				--m_Count;
				if (m_Count > 0)
				{
					// There's more releases on the semaphore. Signal the next thread in line.
					if (HaveWaitingThreads ())
						pthread_cond_signal (&m_Condition);
				}
			}
			break;
	}

	return kWaitStatusSuccess;
}

void PosixWaitObject::InterruptWait ()
{
	pthread_cond_broadcast (&m_Condition);
}

}
}
}

#endif // IL2CPP_TARGET_POSIX
